漏洞详情
Spring Messaging 属于Spring Framework项目,其定义了Enterprise Integration Patterns典型实现的接口及相关的支持(注解,接口的简单默认实现等)
https://pivotal.io/security/cve-2018-1270
影响范围
1 | Spring |
Spring Framework允许应用程序通过spring-messaging模块通过简单的内存STOMP代理通过WebSocket端点公开STOMP。恶意用户(或攻击者)可以向代理发送消息,这可能导致远程执行代码攻击。
复现环境
https://repo.spring.io/release/org/springframework/spring/5.0.0.RELEASE/
SPEL
spel是Spring Expression Language 即,spring表达式语言,是一个支持查询和操作运行时对象导航图功能的强大的表达式语言.支持以下功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19文字表达式
布尔和关系运算符
正则表达式
类表达式
访问 properties, arrays, lists, maps
方法调用
关系运算符
参数
调用构造函数
Bean引用
构造Array
内嵌lists
内嵌maps
三元运算符
变量
用户定义的函数
集合投影
集合筛选
模板表达式
一个hello world1
2
3
4
5
6
7
8
9
10
11
12import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class Spel {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
String var1 = (String)parser.parseExpression("'Hello World'").getValue();
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
System.out.println(maxValue);
System.out.println(var1);
}
}
弹calc
代码1
String calc = (String)parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')").getValue();
这里的T是类型操作符,可以从类路径加载指定类名称(全限定名)所对应的 Class 的实例,格式为:T(全限定类名),效果等同于 ClassLoader#loadClass()
Websocket
由于HTTP具有单向通信的特点,于是造成了Server向Client推送消息变得很难,需要使用轮询的方式,于是有了WebSocket,他支持双向通信,类似于聊天室模式,在这个会话里,Server和Clinet都能发送数据,相互通信,所以WebSocket是一种在一个TCP连接上能够全双工,双向通信的协议
Stomp
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,一个非常简单和容易实现的协议,提供了可互操作的连接格式,易于开发并应用广泛。这个协议可以有多种载体,可以通过HTTP,也可以通过WebSocket。在Spring-Message中使用的是STOMP Over WebSocket。
STOMP是一种基于帧的协议, STOMP是基于Text的,但也允许传输二进制数据。 它的默认编码是UTF-8,但它的消息体也支持其他编码方式,比如压缩编码。
一个STOMP帧由三部分组成:命令,Header(头信息),Body(消息体)
1 | COMMAND |
一个实际帧结构1
2
3
4
5SEND
destination:/broker/roomId/1
content-length:57
{“type":"ENTER","content":"o7jD64gNifq-wq-C13Q5CRisJx5E"}
spring 的消息功能是基于消息代理构建的
demo
stomp的架构图
图中各个组件介绍:
- 生产者型客户端(左上组件): 发送SEND命令到某个目的地址(destination)的客户端。
- 消费者型客户端(左下组件): 订阅某个目的地址(destination), 并接收此目的地址所推送过来的消息的客户端。
- request channel: 一组用来接收生产者型客户端所推送过来的消息的线程池。
- response channel: 一组用来推送消息给消费者型客户端的线程池。
- broker: 消息队列管理者,也可以成为消息代理。它有自己的地址(例如“/topic”),客户端可以向其发送订阅指令,它会记录哪些订阅了这个目的地址(destination)。
- 应用目的地址(图中的”/app”): 发送到这类目的地址的消息在到达broker之前,会先路由到由应用写的某个方法。相当于对进入broker的消息进行一次拦截,目的是针对消息做一些业务处理。
- 非应用目的地址(图中的”/topic”,也是消息代理地址): 发送到这类目的地址的消息会直接转到broker。不会被应用拦截。
- SimpAnnotatonMethod: 发送到应用目的地址的消息在到达broker之前, 先路由到的方法. 这部分代码是由应用控制的。
1 | git clone https://github.com/spring-guides/gs-messaging-stomp-websocket |
几个重要的文件
GreetingController.java(相当于图中的SimpAnnotatonMethod)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package hello;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
public class GreetingController {
//使用MessageMapping注解来标识所有发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理
"/hello") (
//使用SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic/greetings”.
"/topic/greetings") (
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
return new Greeting("Hello, " + message.getName() + "!");
}
}
尤其注意,这个处理器方法有一个返回值,这个返回值并不是返回给客户端的,而是转发给消息代理的,如果客户端想要这个返回值的话,只能从消息代理订阅
WebSocketConfig.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
//使用Configuration注解标识这是一个Springboot的配置类.
//使用此注解来标识使能WebSocket的broker.即使用broker来处理消息
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public void configureMessageBroker(MessageBrokerRegistry config) {
//应用程序以 /app 为前缀,而 代理目的地以 /topic 为前缀
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket").withSockJS();
}
}
运行后
漏洞复现
在static/app.js
添加header,需要指定为selector
,在文档里有说明,是一个选择器
https://stomp.github.io/stomp-specification-1.0.html
随便发送一条消息即可触发
可以抓个包看到websocket发送的STOMP帧
点击connect时
点击send时
漏洞分析
SUBSCRIBE
先在ExecutorSubscribableChannel.class
的run
函数下断,可以发现先来的第一条命令是CONNECT
然后是SUBSCRIBE
命令,此时的message如下
然后从header
取出selector
并判断是否为null
不为null后,调用expressionParser.parseExpression
处理selector
,返回给expression
变量,
然后调用subscriptionRegistry.addSubscription
方法添加订阅
传入sessionId
,subsId
,destination
和expression
,首先从sessionid中获得info,如果没有就注册订阅,存储着session里的东西,返回DefaultSubscriptionRegistry
的实例info
然后获取value,这里为null1
DefaultSubscriptionRegistry.SessionSubscriptionInfo value = (DefaultSubscriptionRegistry.SessionSubscriptionInfo)this.sessions.putIfAbsent(sessionId, info);
往下走,添加订阅
把destination put 进destinationLookup
然后调用Subscription初始化id和selector值,最后add进subs变量
然后在缓存中更新订阅
Send
发送的message为
前面的调用很多,我们之间从处理的地方开始跟进
在这之前的调用栈,通过反射来调到我们请求的方法
inovke处理完后,会返回我们的控制器里的值
往下执行,跟入handleReturnValue
调用getReturnValueHandler
来获得handler变量,用于处理return数据,有默认的defaultDestinationPrefix
继续跟进
提取headers等信息
然后get Sessionid
为c01pvxeq
获取destination
进入for循环后,首先调用createHeaders
,把sessionid
传入,主要是设置session和header
然后跟到convertAndSend
调用doConvert函数得到message,然后调用send,开始发送message
调用sendInternal
message
由于timeout为-1,所以调用send传入message参数
跟入sendInternal
调用SimpleBrokerMessageHandler
的handleMessageInternal
获取到信息后调用updateSessionReadTime
更新时间
检测是否是perfix开头,这里发往的订阅目的地是/topic/greetings
,所以checkDestinationPrefix
为true
调用sendMessageToSubscribers
发送message
到订阅地址
跟入findSubscriptions
查找订阅,检测destination
,没有error则进入findSubscriptionsInternal
函数
这里的destinationCache变量是
还记得一开始处理订阅的时候么,我们有一些put操作,相当于存入缓存了,这里就是一个提取的操作
调用DefaultSubscriptionRegistry
的getSubscriptions
获取订阅信息,
从整个订阅里获取全部的信息然后保存到一个迭代器里,并且赋值给info
最后根据sessionID add result里
最后更新缓存
然后跟进filterSubscriptions
首先判断selector
是否存在,存在则进入判断result
变量传入后形参是allMatches
提取出expression
,判断不为空
然后在getValue
执行spel表达式
调用链很长
修复
更改为要重用的静态计算上下文,SimpleEvaluationContext 对于权限的限制更为严格,能够进行的操作更少。只支持一些简单的Map结构。
来看看修改为SimpleEvaluationContext
如下代码的执行情况1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
public class Spel {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression calc = parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')");
calc.getValue(context);
}
}
不再弹出calc。
Reference
- https://my.oschina.net/genghz/blog/1796965
- https://www.toptal.com/java/stomp-spring-boot-websocket
- https://pivotal.io/security/cve-2018-1270
- https://juejin.im/post/5b7071ade51d45665816f8c0
- https://juejin.im/post/5b9cb6825188255c5546e5f4
- https://spring.io/guides/gs/messaging-jms/
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.html
- https://blog.csdn.net/pacosonswjtu/article/details/51914567
- https://www.cnblogs.com/jmcui/p/8999998.html